﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;


namespace Nuts
{
    
    public class Squirrel
    {
        protected enum Direction
        {
            LEFT,
            RIGHT
        };

        protected enum RunningState
        {
            HIDDEN,         // squirrel is hidden in a safe spot
            RUNNING,        // running toward a safe spot
            GRABBED,        // grabbed by something
            FALLING,        // free falling after being let go of a grab
            STUNNED,        // stunned by a fall or a hit
        };

        protected enum BoostState
        {
            NEUTRAL = 0,
            BOOST_0,
            BOOST_1,
            BOOST_2,
            EXHAUSTED,

            Count
        };

        protected float[] RUN_SPEED_FACTORS = { 1.0f, 1.5f, 2.5f, 4.5f, 0.3f };
        protected const float BOOST_TIME = 5.0f;
        protected const float SLAP_SPRITE_TIMER = 0.25f;
        protected const float GRAVITY = 400.0f;
        protected const float FALL_SPEED_STUN_VALUE = 400.0f;

        protected Game1 _game;
        protected Vector2   _position;      // current position of squirrel on screen
        protected Rectangle _hit_box;       // bounding box of squirrel for slaps
        protected Rectangle _collect_box;   // box used to collect new nuts
        protected Direction     _direction;
        protected RunningState  _run_state;
        protected float _timer_hidden;

        protected BoostState _boost_state;
        protected float _boost_timer;       // time left on slap induced boost or exhaustion
        protected float _slap_sprite_timer; // to show slap sprite

        protected float _current_speed;

        protected float _anim_timer;

        Texture2D _tex;
        SoundEffect _snd_slap;
        Rectangle[] _framesPushing;
        Rectangle[] _framesRunning;
        Rectangle   _frameGrabbed;
        Rectangle   _frameSlapped;

        const int FRAME_COUNT_PUSHING = 2;
        const int FRAME_COUNT_RUNNING = 4;

        List<Nut> _collected_nuts;

        public const float HIDDEN_TIME = 1.5f;
        public const float BASE_RUN_SPEED = 200.0f;
        public const float BASE_ACCELERATION = 50.0f;
        public const float BASE_NUT_DRAG = 0.80f; // 20% per nut (multiplicative)

        public const float STUN_DECCELERATION = 200.0f;
        public const float STUN_BASE_DURATION = 3.0f;

        public const int WIDTH = 96;
        public const int HEIGHT = 64;
        const int COLLECT_BOX_BASE_WIDTH = WIDTH / 4;

        const int SHADOW_WIDTH = 80;
        const int SHADOW_THICKNESS = 15;

        public Rectangle GetHitBox()
        {
            return _hit_box;
        }

        public Rectangle GetCollectBox()
        {
            return _collect_box;
        }

        public Squirrel(Game1 game)
        {
            _game = game;
        }

        public void Initialize()
        {
            _run_state = RunningState.HIDDEN;
            _direction = Direction.RIGHT;
            _timer_hidden = HIDDEN_TIME;
            _position.X = _game.SAFE_SPOT_LEFT.X;
            _position.Y = Game1.GROUND_LEVEL - HEIGHT / 2;
            _current_speed = 0.0f;

            _boost_state = BoostState.NEUTRAL;
            _boost_timer = 0.0f;
            _slap_sprite_timer = 0.0f;

            _hit_box = new Rectangle(0, 0, WIDTH, HEIGHT);
            UpdateHitBox();

            _collect_box = new Rectangle(0,0, WIDTH/2, HEIGHT);

            _collected_nuts = new List<Nut>();

            _anim_timer = 0.0f;
        }

        private void UpdateHitBox()
        {
            // position of squirrel is center of sprite
            _hit_box.X = (int)_position.X - WIDTH / 2;
            _hit_box.Y = (int)_position.Y - HEIGHT / 2; // y origin is top
        }

        private void UpdateCollectBox()
        {
            if ( _run_state != RunningState.RUNNING )
            {
                // collapse box
                _collect_box.Width = 0;
                _collect_box.Height = 0;
                return;
            }

            // height is invariant
            _collect_box.Height = _hit_box.Height;
            _collect_box.Y      = _hit_box.Y;

            // find extents of box

            // init min and max with base collect box
            int min_x;
            int max_x;

            int squirrel_right_side = _hit_box.Right;
            int squirrel_left_side = _hit_box.Left;

            if(_direction == Direction.RIGHT)
            {
                min_x = squirrel_right_side - COLLECT_BOX_BASE_WIDTH;
                max_x = squirrel_right_side;

                foreach(Nut nut in _collected_nuts)
                {
                    max_x = Math.Max(nut.GetHitBox().Right, max_x);
                }
            }
            else
            {
                min_x = squirrel_left_side;
                max_x = squirrel_left_side + COLLECT_BOX_BASE_WIDTH;

                // and extent to collected nuts
                foreach(Nut nut in _collected_nuts)
                {
                    min_x = Math.Min(nut.GetHitBox().Left, min_x);
                }
            }

            _collect_box.Width = max_x - min_x;

            // position box on the correct side of the squirrel :D
            if(_direction == Direction.RIGHT)
            {
                // right side minus margin
                _collect_box.X = squirrel_right_side - COLLECT_BOX_BASE_WIDTH;
            }
            else
            {
                // left side minus (width minus margin)
                _collect_box.X = squirrel_left_side - _collect_box.Width + COLLECT_BOX_BASE_WIDTH;
            }
        }

        private void CollectNuts()
        {
            foreach(Nut nut in _game.GetNuts())
            {
                if(!nut.IsOnGround())
                    continue;

                if(_collect_box.Intersects(nut.GetHitBox()))
                    _collected_nuts.Add(nut);
            }
        }

        private void UpdateCollectedNuts(float delta_x)
        {
            // delta_x applied by squirrel on the collected nuts
            foreach(Nut nut in _collected_nuts)
            {
                nut.Roll(delta_x);
            }
        }

        public void Update(GameTime gameTime)
        {
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;

            // UpdateBoost
            switch (_boost_state)
            {
                case BoostState.BOOST_0:
                case BoostState.BOOST_1:
                case BoostState.BOOST_2:
                case BoostState.EXHAUSTED:
                    _boost_timer -= dt;
                    if (_boost_timer <= 0.0f)
                    {
                        _boost_state = BoostState.NEUTRAL;
                        _boost_timer = 0.0f;
                    }
                    break;

                case BoostState.NEUTRAL:
                    break;
            }

            // update hide / run / stun / grab / fall
            switch(_run_state)
            {
                case RunningState.HIDDEN:
                    {
                        _current_speed = 0.0f;
                        _timer_hidden -= dt;
                        if(_timer_hidden <= 0.0f)
                        {
                            // time to run!
                            _run_state = RunningState.RUNNING;
                        }

                        UpdateCollectBox();
                        _collected_nuts.Clear();

                    }
                    break;

                case RunningState.STUNNED:
                    {
                        // come to a halt.
                        float target_speed = 0.0f;

                        // and fast
                        float acceleration_factor = STUN_DECCELERATION;

                        float delta_speed = 0.0f;
                        float delta_pos = 0.0f;
                        IntegrateSpeedAndPosition(target_speed, acceleration_factor, dt, ref delta_speed, ref delta_pos);

                        // use hidden timer to check if stun expired
                        _timer_hidden -= dt;
                        if(_timer_hidden <= 0.0f)
                        {
                            // back on your feet scum!
                            _run_state = RunningState.RUNNING;
                        }

                        UpdateCollectBox();
                        _collected_nuts.Clear();
                    }
                    break;

                case RunningState.GRABBED:
                    {
                        UpdateCollectBox();
                        _collected_nuts.Clear();
                    }
                    break;

                case RunningState.FALLING:
                    {
                        // "integrate"
                        // use current speed as vertical speed :D oh my god you are a dirty programmer.
                        _current_speed += dt * GRAVITY;
                        float drop = _current_speed * dt;                        
                        float lowest_y = _position.Y + drop + HEIGHT/2;
                        // check collision with ground (AND THROW IT ON THE GROUND!)
                        if(lowest_y >= Game1.GROUND_LEVEL)
                        {
                            // ground collision
                            _position.Y = Game1.GROUND_LEVEL - HEIGHT / 2;

                            // play thump sound TODO

                            // stun if speed too high
                            if(_current_speed > FALL_SPEED_STUN_VALUE)
                            {
                                // compute stun factor based on speed TODO
                                Stun();
                            }
                            else
                            {
                                _run_state = RunningState.RUNNING;
                            }

                            // reset speed
                            _current_speed = 0.0f;
                        }
                        else
                        {
                            _position.Y += drop;
                        }

                        UpdateCollectBox();
                        _collected_nuts.Clear();
                    }
                    break;

                case RunningState.RUNNING:
                    {
                        // determine target speed
                        float target_speed = BASE_RUN_SPEED;

                        // boost / fatigue
                        float current_boost_factor = RUN_SPEED_FACTORS[(int)_boost_state];
                        target_speed *= current_boost_factor;

                        // nut drag
                        int nut_count = _collected_nuts.Count;
                        float nut_drag_factor = (float)Math.Pow((double)BASE_NUT_DRAG, (double)nut_count);
                        target_speed *= nut_drag_factor;

                        // determine "acceleration" factor
                        float acceleration_factor = BASE_ACCELERATION;

                        // update velocity and clamp
                        float delta_speed = 0.0f;
                        float delta_pos = 0.0f;
                        IntegrateSpeedAndPosition(target_speed, acceleration_factor, dt, ref delta_speed, ref delta_pos);

                        // update the hit box used to collect new nuts (front half of squirrel + sum of currently collected nuts :D)
                        UpdateCollectBox();
                        _collected_nuts.Clear();
                        CollectNuts();
                        UpdateCollectedNuts(delta_pos);
                    }
                    break;
            }

            UpdateHitBox();

            _game.GetShadowHelper().AddShadow((int)_position.X, _hit_box.Bottom, SHADOW_WIDTH, SHADOW_THICKNESS);
        }

        public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            SpriteEffects flip = SpriteEffects.None;
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;

            // update slap sprite timer
            _slap_sprite_timer -= dt;
            _slap_sprite_timer = Math.Max(0.0f, _slap_sprite_timer);

            // flip sprite to match left/right direction
            if ( _direction == Direction.LEFT )
                flip = SpriteEffects.FlipHorizontally;

            // determine correct animation
            // determine correct frame
            Rectangle src_rect = _framesRunning[0]; // this is our default frame.

            switch(_run_state)
            {
                case RunningState.HIDDEN:
                    src_rect = _framesRunning[0];
                    break;

                case RunningState.FALLING:
                    src_rect = _frameGrabbed;
                    break;

                case RunningState.GRABBED:
                    src_rect = _frameGrabbed;
                    break;

                case RunningState.STUNNED:
                    src_rect = _frameSlapped;
                    break;

                case RunningState.RUNNING:
                    {
                        // update movement based anims timer
                        const float ANIM_DISTANCE = 80.0f; // distance parcourue par l'anim en 1 seconde, en pixels

                        // current speed is in pixels/s            
                        _anim_timer += (_current_speed * dt) / ANIM_DISTANCE;
                        while(_anim_timer >= 1.0f)
                            _anim_timer -= 1.0f;

                        // slapped
                        if(_slap_sprite_timer > 0.0f)
                        {
                            src_rect = _frameSlapped;
                        }
                        // pushing
                        else if(_collected_nuts.Count != 0)
                        {
                            int frame = (int)(_anim_timer * (float)FRAME_COUNT_PUSHING);
                            src_rect = _framesPushing[frame];
                        }
                        // running
                        else
                        {
                            int frame = (int)(_anim_timer * (float)FRAME_COUNT_RUNNING);
                            src_rect = _framesRunning[frame];
                        }
                    }
                    break;
            }

            // position it accordingly
            Rectangle dst_rect = new Rectangle((int)_position.X - src_rect.Width / 2, (int)_position.Y - src_rect.Height / 2, src_rect.Width, src_rect.Height);
            spriteBatch.Draw(_tex, dst_rect, src_rect, Color.White, 0.0f, Vector2.Zero, flip, 0.0f);
        }

        public void LoadContent()
        {
            _tex = _game.Content.Load<Texture2D>("squirrel_00");
            _snd_slap = _game.Content.Load<SoundEffect>("sound_hit");

            // construct animation frames
            _framesPushing = new Rectangle[FRAME_COUNT_PUSHING];
            _framesPushing[0] = new Rectangle(0, 64, 104, 64);
            _framesPushing[1] = new Rectangle(106, 64, 106, 64);

            _framesRunning = new Rectangle[FRAME_COUNT_RUNNING];
            _framesRunning[0] = new Rectangle(  0, 0, 102, 64);
            _framesRunning[1] = new Rectangle(104, 0, 104, 64);
            _framesRunning[2] = new Rectangle(212, 0, 104, 64);
            _framesRunning[3] = new Rectangle(316, 0,  96, 64);

            _frameGrabbed = new Rectangle(462, 0, 50, 64);

            _frameSlapped = new Rectangle(220, 64, 92, 64);
        }

        public void OnSlap(int x, int y)
        {
            _snd_slap.Play();
            if (_boost_state < BoostState.EXHAUSTED)
            {
                _boost_state += 1;
            }

            _boost_timer = BOOST_TIME; // refresh "boost"

            // for sprite selection
            _slap_sprite_timer = SLAP_SPRITE_TIMER;
        }

        public void Stun()
        {
            _run_state = RunningState.STUNNED;
            _timer_hidden = STUN_BASE_DURATION;
        }

        public void BeginGrab()
        {
            _run_state = RunningState.GRABBED;
        }

        public void GrabSetPos(int x, int y)
        {
            if(_run_state != RunningState.GRABBED)
                return;

            _position.X = x;
            _position.Y = y;
            // 
        }

        public void EndGrab()
        {
            if(_run_state != RunningState.GRABBED)
                return;

            _run_state = RunningState.FALLING;
            _current_speed = 0.0f;
        }

        private void IntegrateSpeedAndPosition(float target_speed, float acceleration_factor, float dt, ref float out_delta_speed, ref float out_delta_pos)
        {
            float delta_speed = (target_speed - _current_speed);
            _current_speed += delta_speed * acceleration_factor * dt * dt;
            if(Math.Abs(delta_speed) < 0.0001f)
                _current_speed = target_speed;

            // update position from speed
            float delta_pos = _current_speed * dt;

            switch(_direction)
            {
                case Direction.RIGHT:
                    _position.X += delta_pos;
                    if(_position.X >= _game.SAFE_SPOT_RIGHT.X)
                    {
                        _timer_hidden = HIDDEN_TIME;
                        _run_state = RunningState.HIDDEN;
                        _direction = Direction.LEFT;
                    }
                    break;

                case Direction.LEFT:
                    delta_pos *= -1.0f;
                    _position.X += delta_pos;
                    if(_position.X <= _game.SAFE_SPOT_LEFT.X)
                    {
                        _timer_hidden = HIDDEN_TIME;
                        _run_state = RunningState.HIDDEN;
                        _direction = Direction.RIGHT;
                    }
                    break;
            }

            out_delta_speed = delta_speed;
            out_delta_pos = delta_pos;
        }
    }
}
